/*
 * Copyright (c) 2011-2019 Contributors to the Eclipse Foundation
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
 * which is available at https://www.apache.org/licenses/LICENSE-2.0.
 *
 * SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
 */
package io.vertx.tests.http;

import io.netty.handler.proxy.ProxyConnectException;
import io.vertx.core.Future;
import io.vertx.core.VertxOptions;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.http.HttpClientOptions;
import io.vertx.core.http.HttpClientRequest;
import io.vertx.core.http.HttpClientResponse;
import io.vertx.core.http.HttpResponseExpectation;
import io.vertx.core.http.HttpServerOptions;
import io.vertx.core.http.HttpServerRequest;
import io.vertx.core.http.PoolOptions;
import io.vertx.core.http.RequestOptions;
import io.vertx.core.http.WebSocketClient;
import io.vertx.core.http.WebSocketClientOptions;
import io.vertx.core.http.impl.CleanableHttpClient;
import io.vertx.core.http.impl.HttpClientImpl;
import io.vertx.core.net.ProxyOptions;
import io.vertx.core.net.ProxyType;
import io.vertx.core.net.SocketAddress;
import io.vertx.test.http.HttpTestBase;
import io.vertx.test.proxy.*;
import io.vertx.test.tls.Cert;
import org.junit.Assume;
import org.junit.Rule;
import org.junit.Test;

import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.function.Supplier;

import static io.vertx.core.http.HttpMethod.GET;
import static org.hamcrest.CoreMatchers.instanceOf;

public class Http1xProxyTest extends HttpTestBase {

  @Rule
  public Proxy proxy = new Proxy();

  @Override
  protected VertxOptions getOptions() {
    VertxOptions options = super.getOptions();
    options.getAddressResolverOptions().setHostsValue(Buffer.buffer("" +
      "127.0.0.1 localhost\n" +
      "127.0.0.1 www1.example1.com\n" +
      "127.0.0.1 www2.example1.com\n" +
      "127.0.0.1 www1.example2.com\n" +
      "127.0.0.1 www2.example2.com\n"
      ));
    return options;
  }

  @WithProxy(kind = ProxyKind.HTTP)
  @Test
  public void testHttpProxyRequest() throws Exception {
    client.close();
    client = vertx.createHttpClient(new HttpClientOptions()
      .setProxyOptions(new ProxyOptions().setType(ProxyType.HTTP).setHost("localhost").setPort(proxy.port())));
    testHttpProxyRequest(() -> client.request(new RequestOptions()
      .setHost(DEFAULT_HTTP_HOST)
      .setPort(DEFAULT_HTTP_PORT)
      .setURI("/")
    ).compose(HttpClientRequest::send)).onComplete(onSuccess(v -> {
      assertProxiedRequest(DEFAULT_HTTP_HOST);
      testComplete();
    }));
    await();
  }

  @WithProxy(kind = ProxyKind.HTTP)
  @Test
  public void testHttpProxyRequest2() throws Exception {
    testHttpProxyRequest(() -> client.request(new RequestOptions()
      .setProxyOptions(new ProxyOptions().setType(ProxyType.HTTP).setHost("localhost").setPort(proxy.port()))
      .setHost(DEFAULT_HTTP_HOST)
      .setPort(DEFAULT_HTTP_PORT)
      .setURI("/")
    ).compose(HttpClientRequest::send)).onComplete(onSuccess(v -> {
      assertProxiedRequest(DEFAULT_HTTP_HOST);
      testComplete();
    }));
    await();
  }

  @WithProxy(kind = ProxyKind.HTTP)
  @Test
  public void testAcceptFilter() throws Exception {
    testFilter(true);
  }

  @WithProxy(kind = ProxyKind.HTTP)
  @Test
  public void testRejectFilter() throws Exception {
    testFilter(false);
  }

  private void testFilter(boolean accept) throws Exception {
    client.close();
    client = vertx.createHttpClient(new HttpClientOptions()
      .setProxyOptions(new ProxyOptions().setType(ProxyType.HTTP).setHost("localhost").setPort(proxy.port())));
    Set<SocketAddress> filtered = Collections.synchronizedSet(new HashSet<>());
    ((HttpClientImpl)((CleanableHttpClient)client).delegate).proxyFilter(so -> {
      filtered.add(so);
      return accept;
    });
    testHttpProxyRequest(() -> client.request(new RequestOptions()
      .setHost(DEFAULT_HTTP_HOST)
      .setPort(DEFAULT_HTTP_PORT)
      .setURI("/")
    ).compose(HttpClientRequest::send)).onComplete(onSuccess(v -> {
      if (accept) {
        assertProxiedRequest(DEFAULT_HTTP_HOST);
      }
      assertEquals(Collections.singleton(SocketAddress.inetSocketAddress(DEFAULT_HTTP_PORT, "localhost")), filtered);
      testComplete();
    }));
    await();
  }

  @WithProxy(kind = ProxyKind.HTTP)
  @Test
  public void testNonProxyHosts1() throws Exception {
    testNonProxyHosts(Collections.singletonList("www1.example1.com"), "www1.example1.com", false);
  }

  @WithProxy(kind = ProxyKind.HTTP, localhosts = "www2.example1.com")
  @Test
  public void testNonProxyHosts2() throws Exception {
    testNonProxyHosts(Collections.singletonList("www1.example1.com"), "www2.example1.com", true);
  }

  @WithProxy(kind = ProxyKind.HTTP)
  @Test
  public void testNonProxyHosts3() throws Exception {
    testNonProxyHosts(Collections.singletonList("*.example2.com"), "www1.example2.com", false);
  }

  @WithProxy(kind = ProxyKind.HTTP)
  @Test
  public void testNonProxyHosts4() throws Exception {
    testNonProxyHosts(Collections.singletonList("*.example2.com"), "www2.example2.com", false);
  }

  private void testNonProxyHosts(List<String> nonProxyHosts, String host, boolean proxied) throws Exception {
    client.close();
    client = vertx.createHttpClient(new HttpClientOptions()
      .setNonProxyHosts(nonProxyHosts)
      .setProxyOptions(new ProxyOptions().setType(ProxyType.HTTP).setHost("localhost").setPort(proxy.port())));
    testHttpProxyRequest(() -> client.request(new RequestOptions()
      .setHost(host)
      .setPort(DEFAULT_HTTP_PORT)
      .setURI("/")
    ).compose(HttpClientRequest::send)).onComplete(onSuccess(v -> {
      if (proxied) {
        assertProxiedRequest(host);
      }
      testComplete();
    }));
    await();
  }

  @WithProxy(kind = ProxyKind.HTTP)
  @Test
  public void testHttpProxyRequestOverrideClientSsl() throws Exception {
    client.close();
    client = vertx.createHttpClient(new HttpClientOptions()
      .setSsl(true).setProxyOptions(new ProxyOptions().setType(ProxyType.HTTP).setHost("localhost").setPort(proxy.port())));
    testHttpProxyRequest(() -> client
      .request(new RequestOptions().setSsl(false).setHost("localhost").setPort(DEFAULT_HTTP_PORT))
      .compose(HttpClientRequest::send)).onComplete(onSuccess(v -> {
      assertProxiedRequest(DEFAULT_HTTP_HOST);
      testComplete();
    }));
    await();
  }

  private void assertProxiedRequest(String host) {
    assertNotNull("request did not go through proxy", proxy.lastUri());
    assertEquals("Host header doesn't contain target host", host + ":" + DEFAULT_HTTP_PORT, proxy.lastRequestHeaders().get("Host"));
  }

  private Future<?> testHttpProxyRequest(Supplier<Future<HttpClientResponse>> reqFact) throws Exception {
    server.requestHandler(req -> {
      req.response().end();
    });
    return server.listen()
      .compose(s -> reqFact
        .get()
        .expecting(HttpResponseExpectation.SC_OK));
  }

  @WithProxy(username = "user", kind = ProxyKind.HTTP)
  @Test
  public void testHttpProxyRequestAuth() throws Exception {
    client.close();
    client = vertx.createHttpClient(new HttpClientOptions()
      .setProxyOptions(new ProxyOptions().setType(ProxyType.HTTP).setHost("localhost").setPort(proxy.port())
        .setUsername("user").setPassword("user")));

    server.requestHandler(req -> {
      req.response().end();
    });

    server.listen().onComplete(onSuccess(s -> {
      client.request(new RequestOptions()
        .setHost(DEFAULT_HTTP_HOST)
        .setPort(DEFAULT_HTTP_PORT)
        .setURI("/")
      ).onComplete(onSuccess(req -> {
        req.send().onComplete(onSuccess(resp -> {
          assertEquals(200, resp.statusCode());
          assertNotNull("request did not go through proxy", proxy.lastUri());
          assertEquals("Host header doesn't contain target host", DEFAULT_HTTP_HOST_AND_PORT, proxy.lastRequestHeaders().get("Host"));
          testComplete();
        }));
      }));
    }));
    await();
  }

  @WithProxy(kind = ProxyKind.HTTP)
  @Test
  public void testHttpProxyFtpRequest() throws Exception {
    client.close();
    client = vertx.createHttpClient(new HttpClientOptions()
      .setProxyOptions(new ProxyOptions().setType(ProxyType.HTTP).setHost(DEFAULT_HTTP_HOST).setPort(proxy.port())));
    final String url = "ftp://ftp.gnu.org/gnu/";
    proxy.forceUri("http://" + DEFAULT_HTTP_HOST_AND_PORT+ "/");
    server.requestHandler(req -> {
      req.response().end();
    });

    server.listen().onComplete(onSuccess(s -> {
      client.request(new RequestOptions().setURI(url))
        .onComplete(onSuccess(req -> {
        req.send().onComplete(onSuccess(resp -> {
          assertEquals(200, resp.statusCode());
          assertEquals("request did sent the expected url", url, proxy.lastUri());
          testComplete();
        }));
      }));
    }));
    await();
  }

  @WithProxy(kind = ProxyKind.SOCKS5)
  @Test
  public void testHttpSocksProxyRequest() throws Exception {
    client.close();
    client = vertx.createHttpClient(new HttpClientOptions()
      .setProxyOptions(new ProxyOptions().setType(ProxyType.SOCKS5).setHost("localhost").setPort(proxy.port())));

    server.requestHandler(req -> req.response().end());

    startServer();

    client.request(new RequestOptions()
      .setHost(DEFAULT_HTTP_HOST)
      .setPort(DEFAULT_HTTP_PORT)
      .setURI("/")).onComplete(onSuccess(req -> {
      req.send().onComplete(onSuccess(resp -> {
        assertEquals(200, resp.statusCode());
        assertNotNull("request did not go through proxy", proxy.lastUri());
        testComplete();
      }));
    }));
    await();
  }

  @WithProxy(username = "user", kind = ProxyKind.SOCKS5)
  @Test
  public void testHttpSocksProxyRequestAuth() throws Exception {
    client.close();
    client = vertx.createHttpClient(new HttpClientOptions()
      .setProxyOptions(new ProxyOptions().setType(ProxyType.SOCKS5).setHost("localhost").setPort(proxy.port())
        .setUsername("user").setPassword("user")));

    server.requestHandler(req -> {
      req.response().end();
    });

    startServer();

    client.request(new RequestOptions()
      .setHost(DEFAULT_HTTP_HOST)
      .setPort(DEFAULT_HTTP_PORT)
      .setURI("/")).onComplete(onSuccess(req -> {
      req.send().onComplete(onSuccess(resp -> {
        assertEquals(200, resp.statusCode());
        assertNotNull("request did not go through proxy", proxy.lastUri());
        testComplete();
      }));
    }));
    await();
  }

  @Test
  public void testHttpProxyPooling() throws Exception {
    HttpProxy proxy1 = new HttpProxy().port(HttpProxy.DEFAULT_PORT);
    HttpProxy proxy2 = new HttpProxy().port(HttpProxy.DEFAULT_PORT + 1);
    ProxyOptions req1 = new ProxyOptions()
      .setType(ProxyType.HTTP)
      .setHost("localhost")
      .setPort(proxy1.port());
    ProxyOptions req2 = new ProxyOptions()
      .setType(ProxyType.HTTP)
      .setHost("localhost")
      .setPort(proxy2.port());
    List<String> res = testPooling(req1, req2, proxy1, proxy2);
    assertEquals(Set.of(proxy1.lastLocalAddress(), proxy2.lastLocalAddress()), new HashSet<>(res));
  }

  @Test
  public void testHttpProxyPooling2() throws Exception {
    HttpProxy proxy = new HttpProxy().port(HttpProxy.DEFAULT_PORT);
    ProxyOptions req = new ProxyOptions()
      .setType(ProxyType.HTTP)
      .setHost("localhost")
      .setPort(proxy.port());
    List<String> res = testPooling(req, req, proxy);
    assertEquals(new HashSet<>(proxy.localAddresses()), new HashSet<>(res));
  }

  @Test
  public void testHttpProxyAuthPooling1() throws Exception {
    HttpProxy proxy = new HttpProxy().port(SocksProxy.DEFAULT_PORT).username(Arrays.asList("user1", "user2"));
    ProxyOptions req1 = new ProxyOptions()
      .setUsername("user1")
      .setPassword("user1")
      .setType(ProxyType.HTTP)
      .setHost("localhost")
      .setPort(proxy.port());
    ProxyOptions req2 = new ProxyOptions()
      .setUsername("user2")
      .setPassword("user2")
      .setType(ProxyType.HTTP)
      .setHost("localhost")
      .setPort(proxy.port());
    List<String> res = testPooling(req1, req2, proxy);
    assertEquals(proxy.localAddresses(), res);
  }

  @Test
  public void testHttpProxyAuthPooling2() throws Exception {
    HttpProxy proxy = new HttpProxy().port(SocksProxy.DEFAULT_PORT).username(Arrays.asList("user1"));
    ProxyOptions req1 = new ProxyOptions()
      .setUsername("user1")
      .setPassword("user1")
      .setType(ProxyType.HTTP)
      .setHost("localhost")
      .setPort(proxy.port());
    ProxyOptions req2 = new ProxyOptions()
      .setUsername("user1")
      .setPassword("user1")
      .setType(ProxyType.HTTP)
      .setHost("localhost")
      .setPort(proxy.port());
    List<String> res = testPooling(req1, req2, proxy);
    assertEquals(2, proxy.localAddresses().size());
    assertEquals(new HashSet<>(proxy.localAddresses()), new HashSet<>(res));
  }

  @Test
  public void testSocksProxyPooling1() throws Exception {
    SocksProxy proxy1 = new SocksProxy().port(SocksProxy.DEFAULT_PORT);
    SocksProxy proxy2 = new SocksProxy().port(SocksProxy.DEFAULT_PORT + 1);
    ProxyOptions req1 = new ProxyOptions()
      .setType(ProxyType.SOCKS5)
      .setHost("localhost")
      .setPort(proxy1.port());
    ProxyOptions req2 = new ProxyOptions()
      .setType(ProxyType.SOCKS5)
      .setHost("localhost")
      .setPort(proxy2.port());
    List<String> res = testPooling(req1, req2, proxy1, proxy2);
    assertEquals(Set.of(proxy1.lastLocalAddress(), proxy2.lastLocalAddress()), new HashSet<>(res));
  }

  @Test
  public void testSocksProxyPooling2() throws Exception {
    SocksProxy proxy = new SocksProxy().port(SocksProxy.DEFAULT_PORT);
    ProxyOptions req = new ProxyOptions()
      .setType(ProxyType.SOCKS5)
      .setHost("localhost")
      .setPort(proxy.port());
    List<String> res = testPooling(req, req, proxy);
    assertEquals(new HashSet<>(proxy.localAddresses()), new HashSet<>(res));
  }

  @Test
  public void testSocksProxyAuthPooling1() throws Exception {
    SocksProxy proxy = new SocksProxy().port(SocksProxy.DEFAULT_PORT).username(Arrays.asList("user1", "user2"));
    ProxyOptions req1 = new ProxyOptions()
      .setUsername("user1")
      .setPassword("user1")
      .setType(ProxyType.SOCKS5)
      .setHost("localhost")
      .setPort(proxy.port());
    ProxyOptions req2 = new ProxyOptions()
      .setUsername("user2")
      .setPassword("user2")
      .setType(ProxyType.SOCKS5)
      .setHost("localhost")
      .setPort(proxy.port());
    List<String> res = testPooling(req1, req2, proxy);
    assertEquals(new HashSet<>(proxy.localAddresses()), new HashSet<>(res));
  }

  @Test
  public void testSocksProxyAuthPooling2() throws Exception {
    SocksProxy proxy = new SocksProxy().port(SocksProxy.DEFAULT_PORT).username(Arrays.asList("user1", "user1"));
    ProxyOptions req1 = new ProxyOptions()
      .setUsername("user1")
      .setPassword("user1")
      .setType(ProxyType.SOCKS5)
      .setHost("localhost")
      .setPort(proxy.port());
    ProxyOptions req2 = new ProxyOptions()
      .setUsername("user1")
      .setPassword("user1")
      .setType(ProxyType.SOCKS5)
      .setHost("localhost")
      .setPort(proxy.port());
    List<String> res = testPooling(req1, req2, proxy);
    assertEquals(2, proxy.localAddresses().size());
    assertEquals(new HashSet<>(proxy.localAddresses()), new HashSet<>(res));
  }

  public List<String> testPooling(ProxyOptions request1, ProxyOptions request2, ProxyBase... proxies) throws Exception {
    for (ProxyBase proxy : proxies) {
      proxy.start(vertx);
    }

    client.close();
    client = vertx.createHttpClient(new HttpClientOptions().setKeepAlive(true), new PoolOptions().setHttp1MaxSize(2));

    try {
      List<HttpServerRequest> requests = Collections.synchronizedList(new ArrayList<>());
      server.requestHandler(requests::add)
        .listen()
        .await();

      RequestOptions baseOptions = new RequestOptions()
        .setHost(DEFAULT_HTTP_HOST)
        .setPort(DEFAULT_HTTP_PORT)
        .setURI("/");
      List<String> responses = Collections.synchronizedList(new ArrayList<>());
      for (int i = 0;i < 2;i++) {
        HttpClientRequest request = client
          .request(new RequestOptions(baseOptions).setProxyOptions(i == 0 ? request1 : request2)).await();
        request.end().await();
        request
          .response()
          .expecting(HttpResponseExpectation.SC_OK)
          .compose(HttpClientResponse::body).onComplete(onSuccess(res -> {
            synchronized (responses) {
              responses.add(res.toString());
            }
          }));
      }

      assertWaitUntil(() -> requests.size() == 2);
      for (HttpServerRequest request : requests) {
        int size = responses.size();
        SocketAddress addr = request.connection().remoteAddress();
        request.response().end("" + addr);
        assertWaitUntil(() -> responses.size() == size + 1);
      }
      return responses;
    } finally {
      for (ProxyBase proxy : proxies) {
        proxy.stop();
      }
    }
  }

  @WithProxy(kind = ProxyKind.HTTP)
  @Test
  public void testWssHttpProxy() throws Exception {
    testWebSocket(createBaseServerOptions().setSsl(true)
      .setKeyCertOptions(Cert.SERVER_JKS.get()), new WebSocketClientOptions()
      .setSsl(true)
      .setTrustOptions(Cert.SERVER_JKS.get())
      .setProxyOptions(new ProxyOptions()
        .setType(ProxyType.HTTP)
        .setHost(DEFAULT_HTTP_HOST)
        .setPort(proxy.port())), true);
  }

  @WithProxy(kind = ProxyKind.HTTP)
  @Test
  public void testWsHttpProxy() throws Exception {
    testWebSocket(createBaseServerOptions(), new WebSocketClientOptions()
      .setProxyOptions(new ProxyOptions()
        .setType(ProxyType.HTTP)
        .setHost(DEFAULT_HTTP_HOST)
        .setPort(proxy.port())), true);
  }

  @WithProxy(kind = ProxyKind.SOCKS5)
  @Test
  public void testWssSocks5Proxy() throws Exception {
    testWebSocket(createBaseServerOptions().setSsl(true)
      .setKeyCertOptions(Cert.SERVER_JKS.get()), new WebSocketClientOptions()
      .setSsl(true)
      .setTrustOptions(Cert.SERVER_JKS.get())
      .setProxyOptions(new ProxyOptions()
        .setType(ProxyType.SOCKS5)
        .setHost(DEFAULT_HTTP_HOST)
        .setPort(proxy.port())), true);
  }

  @WithProxy(kind = ProxyKind.SOCKS5)
  @Test
  public void testWsSocks5Proxy() throws Exception {
    testWebSocket(createBaseServerOptions(), new WebSocketClientOptions()
      .setProxyOptions(new ProxyOptions()
        .setType(ProxyType.SOCKS5)
        .setHost(DEFAULT_HTTP_HOST)
        .setPort(proxy.port())), true);
  }

  @WithProxy(kind = ProxyKind.HTTP)
  @Test
  public void testWsNonProxyHosts() throws Exception {
    testWebSocket(createBaseServerOptions(), new WebSocketClientOptions()
      .addNonProxyHost("localhost")
      .setProxyOptions(new ProxyOptions()
        .setType(ProxyType.HTTP)
        .setHost(DEFAULT_HTTP_HOST)
        .setPort(proxy.port())), false);
  }

  private void testWebSocket(HttpServerOptions serverOptions, WebSocketClientOptions clientOptions, boolean proxied) throws Exception {
    server.close();
    server = vertx.createHttpServer(serverOptions);
    server.webSocketHandler(ws -> {
      ws.handler(buff -> {
        ws.write(buff);
        ws.close();
      });
    });
    startServer(SocketAddress.inetSocketAddress(DEFAULT_HTTPS_PORT, DEFAULT_HTTPS_HOST));
    WebSocketClient client = vertx.createWebSocketClient(clientOptions);
    vertx.runOnContext(v -> {
      client.connect(DEFAULT_HTTPS_PORT, DEFAULT_HTTPS_HOST, "/").onComplete(onSuccess(ws -> {
        ws.handler(buff -> {
          ws.close().onComplete(onSuccess(v2 -> {
            if (proxied) {
              assertNotNull("request did not go through proxy", proxy.lastUri());
              if (clientOptions.getProxyOptions().getType() == ProxyType.HTTP) {
                assertEquals("Host header doesn't contain target host", DEFAULT_HTTPS_HOST_AND_PORT, proxy.lastRequestHeaders().get("Host"));
              }
            } else {
              assertNull("request did go through proxy", proxy.lastUri());
            }
            testComplete();
          }));
        });
        ws.write(Buffer.buffer("Hello world"));
      }));
    });
    await();
  }

  @WithProxy(kind = ProxyKind.HTTP)
  @Test
  public void testNoConnectTimeoutHttpProxy() throws Exception {
    testConnectTimeout(false);
  }

  @WithProxy(kind = ProxyKind.SOCKS4)
  @Test
  public void testNoConnectTimeoutSocksProxy() throws Exception {
    testConnectTimeout(false);
  }

  @WithProxy(kind = ProxyKind.SOCKS5)
  @Test
  public void testNoConnectTimeoutSocks5Proxy() throws Exception {
    testConnectTimeout(false);
  }

  @WithProxy(kind = ProxyKind.HTTP)
  @Test
  public void testConnectTimeoutHttpProxy() throws Exception {
    testConnectTimeout(true);
  }

  @WithProxy(kind = ProxyKind.SOCKS4)
  @Test
  public void testConnectTimeoutSocksProxy() throws Exception {
    testConnectTimeout(true);
  }

  @WithProxy(kind = ProxyKind.SOCKS5)
  @Test
  public void testConnectTimeoutSocks5Proxy() throws Exception {
    testConnectTimeout(true);
  }

  private void testConnectTimeout(boolean shouldTimeout) throws Exception {
    server.close();
    HttpServerOptions serverOptions = createBaseServerOptions()
      .setSsl(true)
      .setKeyCertOptions(Cert.SERVER_JKS.get());
    server = vertx.createHttpServer(serverOptions).requestHandler(request -> request.response().end());
    startServer(SocketAddress.inetSocketAddress(DEFAULT_HTTPS_PORT, DEFAULT_HTTPS_HOST));

    Duration delay = Duration.ofSeconds(1);
    proxy.successDelayMillis(delay);

    ProxyOptions proxyOptions = new ProxyOptions()
      .setType(proxy.type())
      .setHost(DEFAULT_HTTP_HOST)
      .setPort(proxy.port())
      .setConnectTimeout(shouldTimeout ? delay.dividedBy(2) : delay.multipliedBy(2));
    HttpClientOptions clientOptions = createBaseClientOptions()
      .setSsl(true)
      .setTrustOptions(Cert.SERVER_JKS.get())
      .setProxyOptions(proxyOptions);
    client = vertx.createHttpClient(clientOptions);

    RequestOptions requestOptions = new RequestOptions(this.requestOptions)
      .setPort(DEFAULT_HTTPS_PORT)
      .setConnectTimeout(delay.multipliedBy(3).toMillis());
    client.request(requestOptions)
      .compose(req -> req.send().compose(HttpClientResponse::body)).onComplete(ar -> {
        if (shouldTimeout) {
          assertTrue(ar.failed());
          assertThat(ar.cause(), instanceOf(ProxyConnectException.class));
        } else {
          assertTrue(ar.succeeded());
        }
        testComplete();
      });

    await();
  }

  @WithProxy(kind = ProxyKind.HTTP)
  @Test
  public void testFollowRedirectsWithProxy() throws Exception {
    waitFor(2);
    String location = "http://" + DEFAULT_HTTP_HOST + ":" + DEFAULT_HTTP_PORT + "/ok";
    server.requestHandler(req -> {
      if (!req.headers().contains("foo", "bar", true)) {
        fail("Missing expected header");
        return;
      }
      assertEquals(Collections.singletonList("bar"), req.headers().getAll("foo"));
      if (req.path().equals("/redirect")) {
        req.response().setStatusCode(301).putHeader("Location", location).end();
      } else {
        req.response().end(req.path());
        complete();
      }
    });

    startServer();
    client.request(
        new RequestOptions(requestOptions)
          .setServer(null)
          .setMethod(GET)
          .setURI("/redirect")
          .setProxyOptions(new ProxyOptions().setPort(proxy.port()))
      )
      .compose(req -> req
        .putHeader("foo", "bar")
        .setFollowRedirects(true)
        .send()
        .compose(resp -> {
          assertEquals(200, resp.statusCode());
          assertEquals(location, proxy.lastUri());
          return resp.body().compose(body -> {
            if (resp.statusCode() == 200) {
              assertEquals(Buffer.buffer("/ok"), body);
            } else {
              assertEquals(Buffer.buffer(), body);
            }
            return Future.succeededFuture();
          });
        })
      ).onSuccess(v -> testComplete());
    await();
  }
}
